package Script;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;

import Calculate.ScriptFormat;
import Error.OffsetError;
import Error.StandardError;

import interfaces.ErrorReport;
import interfaces.Pointer;
import interfaces.PointerFactory;
import interfaces.PointerReader;
import interfaces.ScriptDumper;
import interfaces.TableFile;
import interfaces.ErrorReport.ErrorType;
import Language.Language;
import Misc.Assert;
import Pointers.JPointerFactory;
import Pointers.JPointerFactory.PointerType;

/**
 * Used to dump binary data to a textfile,
 * converted using a tablefile.
 * 
 * @author Tobias Edin
 */
public class JScriptDumper implements ScriptDumper {
	private File 					gamefile;
	private File 					pointerfile;
	private ArrayList<ErrorReport> 	err;
	private BufferedWriter 			bw;
	private String					endTag = ScriptFormat.getEndtag();
	private String					separator = System.getProperty("line.separator");
	
	public JScriptDumper(File gamefile) {
		this.gamefile = gamefile;
		this.pointerfile = gamefile;
		this.err = new ArrayList<ErrorReport>();
	}
	
	public JScriptDumper(File gamefile, File pointerfile) {
		this.gamefile = gamefile;
		this.pointerfile = pointerfile;
		this.err = new ArrayList<ErrorReport>();
	}
	
	@Override
	public ArrayList<ErrorReport> dump(Offset textStart, Offset textStop,
									long header, ArrayList<Offset> pOffsets, 
									PointerType pType, long textBlock,
									JTableFile table, String filePath,
									int fixedValueLength, 
									byte fixedValuePadding) throws FileNotFoundException, IOException {
		
		File						script;
		PointerReader 				pReader;
		PointerFactory				factory;
		HashMap<Integer, Pointer> 	pMap;
		ArrayList<Pointer> 			pList;
		Pointer						tempPointer;
		Pointer						first;
		Pointer 					second;
		long						start;
		long						stop;
		long 						firstOffset;
		long						secondOffset;
		
		/* Check parameter validity. */
		Assert.notNull(textStart, "Object was null");
		Assert.notNull(textStop, "Object was null");
		Assert.notNull(pOffsets, "Object was null");
		Assert.notNull(table, "Object was null");
		Assert.notNull(gamefile, "Object was null");
		Assert.notNull(filePath, "Object was null");
		Assert.notNull(pointerfile, "Object was null");
		Assert.notNull(pType, "Object was null");
		
		start = textStart.getValue();
		stop = textStop.getValue();
		
		/* Set up script. */
		script = new File(filePath);
		//fw = new FileWriter(script, true); // True: Set to append mode;
		//bw = new BufferedWriter(fw);
		bw = new BufferedWriter(
				new OutputStreamWriter(
						new FileOutputStream(script), table.getCharset()));
			
		if (!script.exists()) { // Create new if file doesn't exist
			script.createNewFile();
				
		} else if (script.exists()) { // Reset content if script present
			script.delete();
			script.createNewFile();
				
		} else if (!script.canWrite()) {
			err.add(new StandardError(Language.scriptWriteError, 
										  ErrorType.ERROR));
			return err;
		}
		
		/* 
		 * If fixed length is used, skip remaining parts of the method and
		 * dump using another algorithm.
		 */
		if (fixedValueLength > 0) {
			return fixedLengthDump(script, table, textStart, textStop, 
							fixedValueLength, fixedValuePadding);
		}
		
		/* Set up pointer reader and retrieve pointers. */
		factory = new JPointerFactory(pType);
		pReader = new JPointerReader(pointerfile, pOffsets, factory);
		pMap = pReader.getPointers();
		pList = new ArrayList<Pointer>(pMap.values());
		
		/* Sort the pointer list. Needed to dump text areas. */
		Collections.sort(pList);
		
		/* 
		 * Set nil byte values in the text address block, based 
		 * on pointer size. 
		 */
		textBlock = setTextAddressBlock(textBlock, factory.pointerSize(), false);
		
		/*
		 * Remove all pointers that doesn't fall within the start and
		 * stop offsets.
		 * 
		 * Subtract 1 since stop is exclusive.
		 */
		pList = dropOutsideRange(pList, start, (stop - 1), textBlock, header);
		
		/* 
		 * Check if there are no pointers. In that case, dump all bytes
		 * between text start and stop.
		 */
		if (pList.isEmpty()) {
			dumpBytes(script, table, textStart.getValue() + textBlock, 
					  textStop.getValue() + textBlock);
			bw.close();
			return err;
		}

		/* 
		 * If there are pointers, check if there's a block
		 * of text in between the text start offset, and the
		 * first pointer. Dump it.
		 */
		tempPointer = pList.get(0);
		if (start < (tempPointer.getValue() + textBlock + header)) {
			dumpBytes(script, table, start, 
					  (tempPointer.getValue() + textBlock + header));
		}
		
		/*
		 * While there's at least two pointers left in the list,
		 * dump all text between the foremost pointer offset,
		 * to the next pointers offset -1 (since it's inclusive).
		 */
		while (pList.size() > 1) {
			
			/* 
			 * Write the pointer, or multiple pointers, if there's
			 * more than one pointing here.
			 */
			do {
				first = pList.get(0);
				second = pList.get(1);
				bw.write("<PT" + printPosition(first.getPosition()) + ">");
				pList.remove(0);

			} while (first.getValue() == second.getValue() && pList.size() > 1);
			
			
			/* Write the text between this pointer and the next. */
			firstOffset = textBlock + first.getValue() + header;
			secondOffset = textBlock + second.getValue() + header;
			
			dumpBytes(script, table, firstOffset, secondOffset);
		}
		
		/*
		 * When there's only one pointer left, dump all text
		 * between it and the stop offset.
		 */
		first = pList.get(0);
		bw.write("<PT" + printPosition(first.getPosition()) + ">");
		dumpBytes(script, table, textBlock + first.getValue() + header, stop);
		
		/* Flush the buffer and close file. */
		bw.close();

		return err;
	}
	
	/**
	 * Dumps a set of bytes using a fixed value length,
	 * without any padding bytes.
	 * 
	 * @param script Script file to dump to.
	 * @param table Table file to handle byte conversions. Won't use DTE entries.
	 * @param start Text start offset to dump from.
	 * @param stop Text stop offset.
	 * @param fixedLength Length of each string to read. Offsets must be of
	 * 					  such a length that an even number of strings fit
	 * 					  between them.
	 * @param fixedValuePadding The byte value that the fixed length strings
	 * 							are padded with in the game file.
	 * @return List of errors occured during dumping.
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	private ArrayList<ErrorReport> fixedLengthDump(File script, TableFile table,
												   Offset start, Offset stop,
												   int fixedLength, 
												   byte fixedValuePadding) 
												   throws FileNotFoundException,
												   IOException {
		
		long 				currentOffset = 	start.getValue();
		boolean				end = 				false;
		byte[]				b = new byte[1]; // Used to read temporary data into
		ArrayList<Offset> 	offList;
		RandomAccessFile	readFile = new RandomAccessFile(gamefile, "rw");
		String				print;
		
		/* Remove DTE from table. */
		table.removeValues(2);
		
		/* Add offsets to a list. */
		offList = new ArrayList<Offset>();
		offList.add(start);
		offList.add(stop);
		
		/* 
		 * Check that the dumping offsets specify exactly an amount
		 * of fixed length data.
		 */
		if ((stop.getValue() - start.getValue()) % fixedLength != 0) {
			err.add(new OffsetError(
						Language.unevenFixedLength, offList, ErrorType.ERROR));
			bw.close();
			return err;
		}
		
		/* 
		 * Keep dumping n bytes at the time until there's
		 * less than n bytes left.
		 */
		while ((currentOffset + fixedLength) <= stop.getValue()) {
			end = false;
			
			/* Read length number of bytes individually. */
			for (long i = currentOffset; 
				 	  i < (currentOffset + fixedLength) && !end; i++) {
				
				readFile.seek(i);
				readFile.read(b);
				print = table.getString(b);
				
				/* If this is not a padding byte, print it. */
				if (print != null && b[0] != fixedValuePadding) {
					bw.write(print);
					
					if (print.equals(endTag)) {
						end = true;
					}
				
				/* 
				 * If it is the first padding byte found, print end tag.
				 * Otherwise, padded bytes shouldn't be printed. Also
				 * print the tag if no end byte has been found yet,
				 * but this is the n:th byte.
				 */
				} else if (b[0] == fixedValuePadding) {
					if (!end) {
						bw.write(endTag + separator);
						end = true;
					}
					
				/* Otherwise, print the byte value. */
				} else if (print == null) {
					bw.write(ScriptFormat.getByteTag(b));
				}
			}
			
			/* If this was a full string, print the end tag. */
			if (!end) {
				bw.write(endTag + separator);
			}
			currentOffset += fixedLength;
		}
		
		bw.close();
		readFile.close();
		return err;
	}
	
	/**
	 * Dumps a number of bytes into a scriptfile.
	 * 
	 * @param scriptFile Script to dump text into.
	 * @param table Tablefile to use for conversions.
	 * @param start Starting offset in game file (inclusive).
	 * @param stop Ending offset in game file (inclusive).
	 * @return True if bytes were dumped correctly.
	 * @throws FileNotFoundException 
	 */
	private void dumpBytes(File scriptFile, TableFile table, long start, 
						long stop) throws FileNotFoundException, IOException {
		RandomAccessFile 	readFile;
		long 				currentOffset = start;
		int 				byteLength = table.getHexMaxLength();
		byte[]				data;
		String				tableText = "";
		int 				i;
		
		/* Create read file. */
		readFile = new RandomAccessFile(gamefile, "r");
		
		/* Read data from file into a string. */
		i = (int) Math.min((stop - currentOffset), byteLength); 
		while (currentOffset < stop) {
			
			/* Jump to offset. */
			readFile.seek(currentOffset);
			
			/* 
			 * Read bytes. Print any matches.
			 */
			data = new byte[i];
			readFile.read(data);
			tableText = table.getString(data);
			
			if (tableText != null) {
				bw.write(tableText);
				currentOffset += i;
				i = (int) Math.min((stop - currentOffset), byteLength);
				
			} else if (i == 1) {
				
				/* 
				 * If only one byte were read, there's no match. Print
				 * a hexadecimal value instead.
				 */
				bw.write(ScriptFormat.getByteTag(data));
				currentOffset += i;
				i = (int) Math.min((stop - currentOffset), byteLength);
				
			} else {
				
				/* 
				 * If there's no match, but the byte amount isn't 1 yet,
				 * decrease the amount.
				 */
				i--;
			}	
		}
		
		readFile.close();
	}
	
	
	/**
	 * Masks a number of bits in a value to 0.
	 * @param value Value to mask bytes in.
	 * @param byteSize Number of bytes to mask.
	 * @param action True if inner bits should be set to 0, false if outer.
	 * @return value with bits masked.
	 */
	private long setTextAddressBlock(long value, int byteSize, boolean action) {
		
		/* Set the number of bytes to mask out. FF, FFFF, FFFFFF etc. */
		long mask = (long) Math.pow(2, (byteSize*8)) - 1;
		
		if(action)
			value = mask & value; // Save only the innermost bits.
		else
			value = (mask | value) ^ mask; // Unset only the innermost bits.
		
		return value;
	}
	
	/**
	 * Formats a pointer position by adding 0's at the end
	 * of the numbered string. Any dumped pointer
	 * should always consists of at least 4 integers.
	 */
	private String printPosition(Long position) {
		String format;
		
		if (position < 10) {
			format = "%04d";
		} else if (position < 100) {
			format = "%03d";
		} else if (position < 1000) {
			format = "%02d";
		} else {
			return Long.toString(position);
		}
		return String.format(format, position);
	}
	
	private ArrayList<Pointer> dropOutsideRange(ArrayList<Pointer> pointerList,
												long start, long stop, 
												long textBlock, long header) {
		Pointer p;
		long value;
		for (int i = 0; i < pointerList.size(); i++) {
			p = pointerList.get(i);
			value = (p.getValue() + textBlock + header);
			
			if (value < start || value > stop) {
				pointerList.remove(i);
				i--;
			}
		}
		return pointerList;
	}

	
	private boolean offsetValidation(Offset textStart, Offset textStop) {
		if (textStart == null || textStop == null) {
			return false;
		}
		
		if (textStart.getValue() > textStop.getValue()) {
			return false;
		}
		
		return true;
	}

}
